package io.github.wanggit.antrpc.console.service.impl;

import io.github.wanggit.antrpc.console.service.GraphService;
import io.github.wanggit.antrpc.console.service.dto.AppRelNodeDTO;
import io.github.wanggit.antrpc.console.web.constants.WebConstants;
import io.github.wanggit.antrpc.console.web.utils.RandomColorUtil;
import io.github.wanggit.antrpc.console.web.vo.CallLogTree;
import io.github.wanggit.antrpc.console.web.vo.CallLogsVO;
import io.github.wanggit.antrpc.console.web.vo.GraphVO;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.WildcardQueryBuilder;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Component
public class GraphServiceImpl implements GraphService {

    @Autowired private RestHighLevelClient restHighLevelClient;

    @Override
    public GraphVO getApplicationGraph(String qkw) {
        DateTime stardand = new DateTime();
        Date endDate = stardand.toDate();
        Date startDate = stardand.minusHours(1).toDate();
        Set<AppRelNodeDTO> appRelNodeDTOS = new HashSet<>();
        try {
            Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
            SearchRequest searchRequest = new SearchRequest(WebConstants.ELASTIC_CALL_LOGS_IDX);
            searchRequest.scroll(scroll);
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            RangeQueryBuilder rangeQueryBuilder =
                    QueryBuilders.rangeQuery("date")
                            .gte(startDate.toInstant().toString())
                            .lt(endDate.toInstant().toString());
            boolQueryBuilder.must(rangeQueryBuilder);
            if (StringUtils.hasText(qkw)) {
                WildcardQueryBuilder appNameFuzzyQueryBuilder =
                        QueryBuilders.wildcardQuery("appName", qkw + "*");
                WildcardQueryBuilder targetAppNameFuzzyQueryBuilder =
                        QueryBuilders.wildcardQuery("targetAppName", qkw + "*");
                boolQueryBuilder.must(
                        QueryBuilders.boolQuery()
                                .should(appNameFuzzyQueryBuilder)
                                .should(targetAppNameFuzzyQueryBuilder));
            }
            searchSourceBuilder.query(boolQueryBuilder).size(10000);
            searchSourceBuilder.fetchSource(
                    new String[] {"appName", "targetAppName"}, new String[] {});
            searchRequest.source(searchSourceBuilder);
            SearchResponse searchResponse =
                    restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            String scrollId = searchResponse.getScrollId();
            SearchHit[] searchHits = searchResponse.getHits().getHits();
            if (null != searchHits) {
                fullSearchHitsToSet(appRelNodeDTOS, searchHits);
            }
            while (searchHits != null && searchHits.length > 0) {
                SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
                scrollRequest.scroll(scroll);
                searchResponse = restHighLevelClient.scroll(scrollRequest, RequestOptions.DEFAULT);
                scrollId = searchResponse.getScrollId();
                searchHits = searchResponse.getHits().getHits();
                fullSearchHitsToSet(appRelNodeDTOS, searchHits);
            }
            ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
            clearScrollRequest.addScrollId(scrollId);
            ClearScrollResponse clearScrollResponse =
                    restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
            boolean succeeded = clearScrollResponse.isSucceeded();
            if (!succeeded && log.isErrorEnabled()) {
                log.error("elasticsearch scroll search error. not closed.");
            }
        } catch (ElasticsearchStatusException ese) {
            log.error(ese.getMessage(), ese);
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("query application call relation error.", e);
            }
        }
        GraphVO graphVO = new GraphVO();
        List<GraphVO.Edge> edges = new ArrayList<>();
        List<GraphVO.Node> nodes = new ArrayList<>();
        Map<String, AtomicInteger> nodeMaps = new HashMap<>();
        appRelNodeDTOS.forEach(
                node -> {
                    calcNodeCount(nodeMaps, node.getFrom());
                    calcNodeCount(nodeMaps, node.getTo());
                    GraphVO.Edge edge = new GraphVO.Edge();
                    edge.setSourceID(node.getFrom());
                    edge.setTargetID(node.getTo());
                    edges.add(edge);
                });
        graphVO.setEdges(edges);
        nodeMaps.forEach(
                (nodeName, count) -> {
                    GraphVO.Node node = new GraphVO.Node();
                    node.setLabel(nodeName);
                    node.setId(nodeName);
                    node.setSize(calcSize(count.intValue()));
                    node.setColor(RandomColorUtil.randomHexColor());
                    nodes.add(node);
                });
        graphVO.setNodes(nodes);
        return graphVO;
    }

    @Override
    public CallLogsVO findCallLogsVOByRequestId(String requestId) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.must(QueryBuilders.matchPhraseQuery("requestId", requestId));
        searchSourceBuilder.query(boolQueryBuilder).fetchSource(true).size(1);
        SearchRequest searchRequest = new SearchRequest(WebConstants.ELASTIC_CALL_LOGS_IDX);
        searchRequest.source(searchSourceBuilder);
        try {
            SearchResponse searchResponse =
                    restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] searchHits = searchResponse.getHits().getHits();
            if (searchHits.length == 1) {
                return CallLogsVO.from(searchHits[0]);
            }
        } catch (ElasticsearchStatusException ese) {
            log.error(ese.getMessage(), ese);
        } catch (IOException e) {
            if (log.isErrorEnabled()) {
                log.error("query from elasticsearch error. requestId=" + requestId, e);
            }
        }
        return null;
    }

    @Override
    public CallLogTree callGraphDatas(String serialNumber, String requestId) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder
                .query(
                        QueryBuilders.boolQuery()
                                .filter(
                                        QueryBuilders.matchPhraseQuery(
                                                "serialNumber", serialNumber)))
                .size(200)
                .fetchSource(true);
        SearchRequest searchRequest = new SearchRequest(WebConstants.ELASTIC_CALL_LOGS_IDX);
        searchRequest.source(searchSourceBuilder);
        Map<String, List<CallLogsVO>> callLogsVOMap = new HashMap<>();
        try {
            SearchResponse searchResponse =
                    restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] searchHits = searchResponse.getHits().getHits();
            for (SearchHit searchHit : searchHits) {
                CallLogsVO callLogsVO = CallLogsVO.from(searchHit);
                if (!callLogsVOMap.containsKey(callLogsVO.getCaller())) {
                    callLogsVOMap.put(callLogsVO.getCaller(), new ArrayList<>());
                }
                callLogsVOMap.get(callLogsVO.getCaller()).add(callLogsVO);
            }
        } catch (ElasticsearchStatusException ese) {
            log.error(ese.getMessage(), ese);
        } catch (IOException e) {
            if (log.isErrorEnabled()) {
                log.error("query from elasticsearch error.", e);
            }
        }
        if (callLogsVOMap.isEmpty()) {
            return new CallLogTree();
        }
        List<CallLogsVO> roots = callLogsVOMap.get("root");
        if (roots.size() != 1) {
            if (log.isErrorEnabled()) {
                log.error(
                        "serialNumber = "
                                + serialNumber
                                + ", not found or found many the requestId=root.");
            }
            return new CallLogTree();
        }
        CallLogsVO root = roots.get(0);
        CallLogTree rootNode = new CallLogTree();
        rootNode.setName("root\n" + root.getAppName());
        rootNode.setValue(root.getPreviousStackTrace());
        getTreeNode(rootNode, "root", callLogsVOMap);
        return rootNode;
    }

    private void getTreeNode(
            CallLogTree parentNode, String caller, Map<String, List<CallLogsVO>> callLogsVOMap) {
        List<CallLogsVO> callLogsVOS = callLogsVOMap.get(caller);
        if (null != callLogsVOS && !callLogsVOS.isEmpty()) {
            for (CallLogsVO callLogsVO : callLogsVOS) {
                CallLogTree treeNode = new CallLogTree();
                treeNode.setName(callLogsVO.getRequestId() + "\n" + callLogsVO.getTargetAppName());
                treeNode.setValue(
                        callLogsVO.getTargetClassName() + "#" + callLogsVO.getTargetMethodName());
                parentNode.addChild(treeNode);
                getTreeNode(treeNode, callLogsVO.getRequestId(), callLogsVOMap);
            }
        }
    }

    private Double calcSize(int basicSize) {
        return Math.log1p(basicSize * 20.0D) * basicSize;
    }

    private void calcNodeCount(Map<String, AtomicInteger> nodes, String key) {
        if (!nodes.containsKey(key)) {
            nodes.put(key, new AtomicInteger(0));
        }
        nodes.get(key).incrementAndGet();
    }

    private void fullSearchHitsToSet(Set<AppRelNodeDTO> appRelNodeDTOS, SearchHit[] searchHits) {
        for (SearchHit searchHit : searchHits) {
            Map<String, Object> sourceAsMap = searchHit.getSourceAsMap();
            Object appName = sourceAsMap.get("appName");
            Object targetAppName = sourceAsMap.get("targetAppName");
            if (null != appName && null != targetAppName) {
                appRelNodeDTOS.add(new AppRelNodeDTO(appName.toString(), targetAppName.toString()));
            }
        }
    }
}
